<?php
/**
 * 邮件相关
 *
 * 本包提供发送邮件相关功能，包括，发送带附件的邮件，使用SMTP发送邮件等
 *
 * @author    Garbin
 * @package    Library
 * @subpackage Ultity
 */

/**
 * EMail对象，用于生成支持各种功能的EMail报文
 *
 * 用例
 * <code>
 * // 创建email类
 * $email = new EMail('garbinh@gmail.com', 'haha', 'OK');
 * // 添加附件
 * $email->addAttch();
 * </code>
 *
 * @author Garbin
 */
class EMail
{
    protected $_mail = array(
        'to'       => array(),
        'subject'  => '',
        'contents' => '',
        'from'     => '',
        'charset'  => 'utf-8',
        'cc'       => array(),
        'bcc'      => array(),
        'attach'   => array(),
    );
    protected $_line_breaker = "\r\n";

    public function __construct($to, $subject = '', $contents = '', $from = '', $charset = 'utf-8')
    {
        $this->_mail['to']       = explode(',', $to);
        $this->_mail['subject']  = $subject;
        $this->_mail['contents'] = $contents;
        $this->_mail['from']     = $from;
        $this->_mail['charset']  = $charset;
    }

    /**
     * 抄送
     *
     * 添加抄送人，多个抄送人用半角“,”号隔开
     *
     * @author Garbin
     * @access public
     * @param  string $cc_list 抄送列表
     * @return void
     **/
    public function cc($cc_list)
    {
        $this->_mail['cc'] = explode(',', $cc_list);
    }

    /**
     * 密送列表
     *
     * 添加密送人，多个密送人用半角“,”号隔开
     *
     * @author Garbin
     * @access public
     * @param  string $bcc_list 密送列表
     * @return void
     **/
    public function bcc($bcc_list)
    {
        $this->_mail['bcc'] = explode(',', $bcc_list);
    }

    /**
     * 添加附件
     *
     * 添加附件至email，若发送后对方不能正常打开，可尝试手工指定mine_type。可通过多次调用添加多个附件。
     *
     * @author Garbin
     * @access public
     * @param  string $path  附件路径
     * @param  string $mime_type 附件的MIME类型
     * @return void
     **/
    public function attach($path, $mime_type = null)
    {
        if (!is_file($path))
        {
            throw new Exception('The attach is not a file');
        }
        $mime_type === null && $mime_type = mime_content_type($path);
        $this->_mail['attach'][] = array('mime' => $mime_type, 'path' => $path);
    }


    /**
     * 获取编码后的数据项
     *
     * 获取指定的email源码及SMTP相关信息
     *
     * @author Garbin
     * @access public
     * @param  string $item 指定获取的项
     * @return string 返回项目内容
     **/
    public function getMeta($item)
    {
        $return = '';
        $charset = $this->getCharset();
        switch ($item)
        {
            case 'subject':
                $return = $this->_parseCharset($this->_mail['subject'], $charset);
                break;
            case 'to':
                foreach ($this->_mail['to'] as $to)
                {
                    $return .= "," . $this->_parseMailAddress($to, $charset);
                }

                $return = substr($return, 1);
                break;
            case 'cc':
                if (!empty($this->_mail['cc']))
                {
                    foreach ($this->_mail['cc'] as $cc)
                    {
                        $return .= "," . $this->_parseMailAddress($cc, $charset);
                    }

                    $return = substr($return, 1);
                }
                break;
            case 'bcc':
                if (!empty($this->_mail['bcc']))
                {
                    foreach ($this->_mail['bcc'] as $bcc)
                    {
                        $return .= "," . $this->_parseMailAddress($bcc, $charset);
                    }

                    $return = substr($return, 1);
                }
                break;
            case 'from':
                if (!empty($this->_mail['from']))
                {
                    $return = $this->_parseMailAddress($this->_mail['from'], $charset);
                }

                break;
            case 'smtp_from':
                if (!empty($this->_mail['from']))
                {
                    $matches = array();
                    if (preg_match('/(.*)\s*<(.*)>\s*/', $this->_mail['from'], $matches))
                    {
                        $return = $matches[2];
                    }
                    else
                    {
                        $return = $this->_mail['from'];
                    }
                }

                break;
            case 'smtp_rcpt':
                $tmp = array_merge($this->_mail['to'], $this->_mail['cc'], $this->_mail['bcc']);
                $return = array();
                foreach ($tmp as $addr)
                {
                    $matches = array();
                    if (preg_match('/(.*)\s*<(.*)>\s*/', $addr, $matches))
                    {
                        $return[] = $matches[2];
                    }
                    else
                    {
                        $return[] = $addr;
                    }
                }
                break;

        return $return;
            default:
                break;
        }

        return $return;
    }

    /**
     * 获取邮件源码
     *
     * 获取邮件源码
     *
     * @author Garbin
     * @access public
     * @return string
     **/
    public function getSource()
    {
        return $this->__toString();
    }

    /**
     * 以字符串形式输出
     *
     * 返回该email的源代码
     *
     * @author    Garbin
     * @access public
     * @return string
     */
    public function __toString()
    {
        $mail_contents = array();

        // 字符集
        $charset = $this->getCharset();

        $mail_contents[] = "MIME-Version: 1.0";

        // Subject
        $mail_contents[] = 'Subject:' . $this->getMeta('subject');

        // From
        $from = $this->getMeta('from');
        if (!empty($from))
        {
            $mail_contents[] = 'From:' . $from;
        }

        $mail_contents[] = 'To:' . $this->getMeta('to');

        // CC
        $cc = $this->getMeta('cc');
        if (!empty($cc))
        {
            $mail_contents[] = "Cc:{$cc}";
        }

        // BCC
        $bcc = $this->getMeta('bcc');
        if (!empty($bcc))
        {
            $mail_contents[] = "Bcc:{$bcc}";
        }

        // 生成块分隔符
        $boundary = md5(uniqid(rand()));

        // Content-type
        if (!empty($this->_mail['attach']))
        {
            $mail_contents[] = 'Content-Type:multipart/mixed;boundary=' . $boundary;
        }
        else
        {
            $mail_contents[] = 'Content-Type:multipart/alternative;boundary=' . $boundary;
        }

        // Contents
        $mail_contents[] = "{$this->_line_breaker}--{$boundary}{$this->_line_breaker}Content-Type:text/html;charset={$charset}{$this->_line_breaker}Content-Transfer-Encoding:base64{$this->_line_breaker}{$this->_line_breaker}" . base64_encode($this->_mail['contents']) . "{$this->_line_breaker}";

        // 附件
        if (!empty($this->_mail['attach']))
        {
            foreach ($this->_mail['attach'] as $file)
            {
                $file_contents = chunk_split(base64_encode(file_get_contents($file['path'])));
                $file_name = $this->_parseCharset(basename($file['path']), $charset);
                $mail_contents[] = "--{$boundary}{$this->_line_breaker}Content-Type:{$file['mime']};name=\"{$file_name}\"{$this->_line_breaker}Content-Transfer-Encoding: base64{$this->_line_breaker}Content-Disposition:attachment; filename=\"{$file_name}\"{$this->_line_breaker}{$this->_line_breaker}{$file_contents}{$this->_line_breaker}";
            }
        }

        // 结束
        $mail_contents[] = "--{$boundary}--";

        return implode("{$this->_line_breaker}", $mail_contents);
    }


    /**
     * 处理邮件地址
     *
     * 读取“邮件地址”，并将其编码。
     *
     * @author Garbin
     * @access protected
     * @param  string $address  待处理的邮件地址
     * @return string 编码后的邮件地址
     **/
    protected function _parseMailAddress($address, $charset)
    {
        $return = '';

        if (empty($address))
        {
            return $return;
        }

        // 处理"张三" <zhangsan@test.com>
        preg_match('/([^,]*)\s*<([^>]+)>/', $address, $tmp);
        if (!empty($tmp[1]))
        {
            // "张三" <zhangshan@test.com>
            $return = '"' . $this->_parseCharset($tmp[1], $charset) . '" <' . $tmp[2] . '>';
        }
        else
        {
            // zhangshan@test.com
            $return = $address;
        }

        return $return;
    }

    /**
     * 获取字符集
     *
     * 获取邮件内容字符集
     *
     * @author Garbin
     * @access public
     * @return string   返回邮件内容字符集，默认utf-8
     **/
    public function getCharset()
    {
        return isset($this->_mail['charset']) ? $this->_mail['charset'] : 'utf-8';
    }

    /**
     * 处理字符串的编码问题
     *
     * 将字符串转换成邮件客户端可正确识别的编码格式
     *
     * @author Garbin
     * @access protected
     * @param  string $str 待处理字符串，可能是FROM，TO，或附件的文件名
     * @param  string $charset 所使用的编码
     * @return string   已编码的字符串
     **/
    protected function _parseCharset($str, $charset)
    {
        return "=?{$charset}?B?" . base64_encode($str) . "?=";
    }
}

/**
 * 邮件服务器
 *
 * 邮件服务器管理程序<br />
 * 用例：
 * <code>
 * $mail_server = new MailServer();
 * $mail_server->send($email); // $email为一个EMail对象
 * </code>
 *
 *
 * @author Garbin
 */
class MailServer
{
    protected $_config = null;
    protected $_smtp_config = array();
    public function __construct($config = null)
    {
        if ($config !== null)
        {
            $this->_config = is_string($config) ? parse_url($config) : $config;
        }
    }

    /**
     * 配置SMTP连接
     *
     * 配置SMTP服务器
     *
     * @author Garbin
     * @access public
     * @param  array $config SMTP连接配置
     * @return void
     **/
    public function smtpConfig($config)
    {
        $this->_smtp_config = $config;
    }

    /**
     * 发送邮件
     *
     * 使用发送指定email
     *
     * @author Garbin
     * @access public
     * @param  Object $email EMail对象
     * @return bool
     **/
    public function send($email)
    {
        if ($this->_config === null)
        {
            return mail($email->getMeta('to'), $email->getMeta('subject'), '', $email->getSource());
        }
        else
        {
            $smtp = new SparrowSMTP();

            $smtp->config($this->_smtp_config);

            // 连接SMTP
            $smtp->connect($this->_config);

            // 发送邮件
            return $smtp->send($email->getMeta('smtp_rcpt'), $email->getSource());
        }
    }
}

/**
 * Sparrow提供的SMTP连接类
 *
 * 通过此类方便的对SMTP进行操作
 *
 * @author    Garbin
 */
class SparrowSMTP 
{
    protected $_connection   = null;
    protected $_response_text = '';
    protected $_reject_list = array();
    protected $_connection_config = array(
        'from'  => '',
        'crlf'  => "\r\n",
        'debug' => false,
        'helo'  => 'local.domain',
        'timeout'  => 60,
    );

    /**
     * SMTP连接配置
     *
     * 配置SMTP连接信息，可配置的信息有
     * <code>
     * array(
     *     'from'  => '',
     *     'crlf'  => "\r\n",
     *     'debug' => false,
     *     'helo'  => 'local.domain',
     *     'timeout'  => 60,
     * );
     * </code>
     *
     * @author Garbin
     * @access public
     * @param  array $connection_config SMTP连接配置信息
     * @return void
     **/
    public function config($connection_config)
    {
        $this->_connection_config = array_merge($this->_connection_config, $connection_config);
    }

    /**
     * 连接SMTP服务器
     *
     * 连接指定配置的SMTP服务器<br />
     * 可配置的信息有<br />
     * <code>
     * array(
     *     'host'  => '',
     *     'port'  => '',
     *     'user'  => '',
     *     'pass'  => '',
     * );
     * </code>
     *
     * @author Garbin
     * @access public
     * @param  mixed $config    SMTP服务器配置信息
     * @return bool     连接成功返回true，反之返回false
     **/
    public function connect($config)
    {
        /*
        $config = array(
            'host'  => '',
            'port'  => '',
            'user'  => '',
            'pass'  => '',
        );
        $config 为SMTP主机信息
        $this->_connection_config 为连接配置
        */
        is_string($config) && $config = parse_url($config);

        // 连接到服务器
        $this->_connection = fsockopen($config['host'], $config['port'], $errno, $errstr, $this->_connection_config['timeout']);
        if (!$this->_connection)
        {
            throw new Exception('Connect to SMTP server failed:' . $errstr);
        }
        stream_set_blocking($this->_connection, true);

        /* 请求登录服务器 */
        $command = $config['user'] ? 'EHLO' : 'HELO';
        $this->_request($command . ' ' . $this->_connection_config['helo']);
        $code = $this->_response();
        if (!($code == '220' || $code == '250'))
        {
            throw new Exception('HELO failed:' . $this->_response_text);
        }

        /* 验证登录 */
        if ($config['user'])
        {
            // 默认的from为user
            $this->config(array(
                'from'  => $config['user']
            ));
            if ($this->_request('AUTH LOGIN') && $this->_response() == '334' &&
                $this->_request(base64_encode($config['user'])) && $this->_response() == '334' &&
                $this->_request(base64_encode($config['pass'])) && $this->_response() == '235')
            {
                return true;
            }
            else
            {
                throw new Exception('Login STMP server failed:' . $this->_response_text);
            }
        }

        return true;
    }

    /**
     * 发送邮件
     *
     * 发送邮件至指定收件人
     *
     * @author    Garbin
     * @param     mixed $rcpt_list  收件人列表，可以是以收件人列表数组，也可以是单个收件人字符串
     * @param     string    $mail_body 邮件主体
     * @throws    Exception
     * @return    bool 发送成功返回true，反之为false
     */
    public function send($rcpt_list, $mail_body)
    {
        // MAIL FROM 开启会话
        $this->_request("MAIL FROM <{$this->_connection_config['from']}>");
        if ($this->_response() != '250')
        {
            throw new Exception($this->_response_text);
        }


        // 指定收件人
        if (!is_array($rcpt_list))
        {
            $rcpt_list = array($rcpt_list);
        }
        $goon = false;
        foreach ($rcpt_list as $rcpt)
        {
            $this->_request("RCPT TO <{$rcpt}>");
            if ($this->_response() != '250')
            {
                // 记录无法送达的邮件地址
                $this->_reject_list[] = $rcpt;
            }

            // 如果列表中的收件人可送达，则可进行下一步
            $goon = true;
        }

        // 发送邮件
        $this->_request("DATA{$this->_connection_config['crlf']}");
        if ($this->_response() != '354')
        {
            throw new Exception($this->_response_text);
        }
        $this->_request($mail_body);

        $this->_request("{$this->_connection_config['crlf']}.{$this->_connection_config['crlf']}");
        if ($this->_response() != '250')
        {
            throw new Exception($this->_response_text);
        }

        return true;
    }

    /**
     *    连接到服务器
     *
     *    @author    Garbin
     *    @return    bool   连接成功返回true，反之为false
     */
    protected function _connectServer()
    {
        if (!is_resource($this->_connection))
        {

            return ($this->_response() == '220');
        }

        return true;
    }

    /**
     *    获取响应结果
     *
     *    获取请求响应结果
     *
     *    @author    Garbin
     *    @param     int $whole 默认为0 表示只获取响应的号码，1为获取所有信息
     *    @return    string 返回响应结果
     */
    protected function _response($whole = 0)
    {
        //这里使用fread而不使用fgets的原因是使用fread能将所有信息读完，这样就可以避免下条命令执行时读取的状态是错误的问题
        $str = fread($this->_connection, 512);
        $rzt = '';
        if ($str !== null)
        {
            $this->_response_text = $str;
            if ($this->_connection_config['debug'] && !$whole)
            {
                echo 'GET :' . $str . '<br />';
            }

            $rzt = $whole ? $str : substr($str, 0, 3) ;
        }

        return $rzt;
    }

    /**
     *    使用指定的命令请求SMTP服务器
     *
     *    向SMTP服务器发送指令
     *
     *    @author    Garbin
     *    @param     string $command 指令
     *    @return    bool 成功发送返回true，反之为false
     */
    protected function _request($command)
    {
        if ($this->_connection_config['debug'])
        {
            echo 'PUT :' . $command . '<br />';
        }

        return fwrite($this->_connection, $command . $this->_connection_config['crlf']);
    }

    /**
     *    关闭会话
     *
     *    @author    Garbin
     *    @return    bool   成功返回true,反之为false
     */
    protected function _quit()
    {
        $this->_request('QUIT');
        if ($this->_response() != '221')
        {
            throw new Exception($this->_response_text);
        }

        @fclose($this->_connection);

        return true;
    }

    /**
     * 关闭连接
     *
     * @author Garbin
     * @access public
     * @return bool 成功返回true，反之为false
     **/
    public function close()
    {
        return $this->_quit();
    }
}
